1   /*
2    * Copyright 2002-2013 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.beans.factory.config;
18  
19  import java.lang.reflect.Constructor;
20  import java.lang.reflect.InvocationHandler;
21  import java.lang.reflect.Method;
22  import java.lang.reflect.Proxy;
23  import java.util.Properties;
24  
25  import org.springframework.beans.BeanUtils;
26  import org.springframework.beans.BeansException;
27  import org.springframework.beans.FatalBeanException;
28  import org.springframework.beans.factory.BeanFactory;
29  import org.springframework.beans.factory.BeanFactoryAware;
30  import org.springframework.beans.factory.FactoryBean;
31  import org.springframework.beans.factory.InitializingBean;
32  import org.springframework.beans.factory.ListableBeanFactory;
33  import org.springframework.util.ReflectionUtils;
34  import org.springframework.util.StringUtils;
35  
36  /**
37   * A {@link FactoryBean} implementation that takes an interface which must have one or more
38   * methods with the signatures {@code MyType xxx()} or {@code MyType xxx(MyIdType id)}
39   * (typically, {@code MyService getService()} or {@code MyService getService(String id)})
40   * and creates a dynamic proxy which implements that interface, delegating to an
41   * underlying {@link org.springframework.beans.factory.BeanFactory}.
42   *
43   * <p>Such service locators permit the decoupling of calling code from
44   * the {@link org.springframework.beans.factory.BeanFactory} API, by using an
45   * appropriate custom locator interface. They will typically be used for
46   * <b>prototype beans</b>, i.e. for factory methods that are supposed to
47   * return a new instance for each call. The client receives a reference to the
48   * service locator via setter or constructor injection, to be able to invoke
49   * the locator's factory methods on demand. <b>For singleton beans, direct
50   * setter or constructor injection of the target bean is preferable.</b>
51   *
52   * <p>On invocation of the no-arg factory method, or the single-arg factory
53   * method with a String id of {@code null} or empty String, if exactly
54   * <b>one</b> bean in the factory matches the return type of the factory
55   * method, that bean is returned, otherwise a
56   * {@link org.springframework.beans.factory.NoSuchBeanDefinitionException}
57   * is thrown.
58   *
59   * <p>On invocation of the single-arg factory method with a non-null (and
60   * non-empty) argument, the proxy returns the result of a
61   * {@link org.springframework.beans.factory.BeanFactory#getBean(String)} call,
62   * using a stringified version of the passed-in id as bean name.
63   *
64   * <p>A factory method argument will usually be a String, but can also be an
65   * int or a custom enumeration type, for example, stringified via
66   * {@code toString}. The resulting String can be used as bean name as-is,
67   * provided that corresponding beans are defined in the bean factory.
68   * Alternatively, {@linkplain #setServiceMappings(java.util.Properties) a custom
69   * mapping} between service IDs and bean names can be defined.
70   *
71   * <p>By way of an example, consider the following service locator interface.
72   * Note that this interface is not dependent on any Spring APIs.
73   *
74   * <pre class="code">package a.b.c;
75   *
76   *public interface ServiceFactory {
77   *
78   *    public MyService getService();
79   *}</pre>
80   *
81   * <p>A sample config in an XML-based
82   * {@link org.springframework.beans.factory.BeanFactory} might look as follows:
83   *
84   * <pre class="code">&lt;beans>
85   *
86   *   &lt;!-- Prototype bean since we have state -->
87   *   &lt;bean id="myService" class="a.b.c.MyService" singleton="false"/>
88   *
89   *   &lt;!-- will lookup the above 'myService' bean by *TYPE* -->
90   *   &lt;bean id="myServiceFactory"
91   *            class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
92   *     &lt;property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
93   *   &lt;/bean>
94   *
95   *   &lt;bean id="clientBean" class="a.b.c.MyClientBean">
96   *     &lt;property name="myServiceFactory" ref="myServiceFactory"/>
97   *   &lt;/bean>
98   *
99   *&lt;/beans></pre>
100  *
101  * <p>The attendant {@code MyClientBean} class implementation might then
102  * look something like this:
103  *
104  * <pre class="code">package a.b.c;
105  *
106  *public class MyClientBean {
107  *
108  *    private ServiceFactory myServiceFactory;
109  *
110  *    // actual implementation provided by the Spring container
111  *    public void setServiceFactory(ServiceFactory myServiceFactory) {
112  *        this.myServiceFactory = myServiceFactory;
113  *    }
114  *
115  *    public void someBusinessMethod() {
116  *        // get a 'fresh', brand new MyService instance
117  *        MyService service = this.myServiceFactory.getService();
118  *        // use the service object to effect the business logic...
119  *    }
120  *}</pre>
121  *
122  * <p>By way of an example that looks up a bean <b>by name</b>, consider
123  * the following service locator interface. Again, note that this
124  * interface is not dependent on any Spring APIs.
125  *
126  * <pre class="code">package a.b.c;
127  *
128  *public interface ServiceFactory {
129  *
130  *    public MyService getService (String serviceName);
131  *}</pre>
132  *
133  * <p>A sample config in an XML-based
134  * {@link org.springframework.beans.factory.BeanFactory} might look as follows:
135  *
136  * <pre class="code">&lt;beans>
137  *
138  *   &lt;!-- Prototype beans since we have state (both extend MyService) -->
139  *   &lt;bean id="specialService" class="a.b.c.SpecialService" singleton="false"/>
140  *   &lt;bean id="anotherService" class="a.b.c.AnotherService" singleton="false"/>
141  *
142  *   &lt;bean id="myServiceFactory"
143  *            class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
144  *     &lt;property name="serviceLocatorInterface" value="a.b.c.ServiceFactory"/>
145  *   &lt;/bean>
146  *
147  *   &lt;bean id="clientBean" class="a.b.c.MyClientBean">
148  *     &lt;property name="myServiceFactory" ref="myServiceFactory"/>
149  *   &lt;/bean>
150  *
151  *&lt;/beans></pre>
152  *
153  * <p>The attendant {@code MyClientBean} class implementation might then
154  * look something like this:
155  *
156  * <pre class="code">package a.b.c;
157  *
158  *public class MyClientBean {
159  *
160  *    private ServiceFactory myServiceFactory;
161  *
162  *    // actual implementation provided by the Spring container
163  *    public void setServiceFactory(ServiceFactory myServiceFactory) {
164  *        this.myServiceFactory = myServiceFactory;
165  *    }
166  *
167  *    public void someBusinessMethod() {
168  *        // get a 'fresh', brand new MyService instance
169  *        MyService service = this.myServiceFactory.getService("specialService");
170  *        // use the service object to effect the business logic...
171  *    }
172  *
173  *    public void anotherBusinessMethod() {
174  *        // get a 'fresh', brand new MyService instance
175  *        MyService service = this.myServiceFactory.getService("anotherService");
176  *        // use the service object to effect the business logic...
177  *    }
178  *}</pre>
179  *
180  * <p>See {@link ObjectFactoryCreatingFactoryBean} for an alternate approach.
181  *
182  * @author Colin Sampaleanu
183  * @author Juergen Hoeller
184  * @since 1.1.4
185  * @see #setServiceLocatorInterface
186  * @see #setServiceMappings
187  * @see ObjectFactoryCreatingFactoryBean
188  */
189 public class ServiceLocatorFactoryBean implements FactoryBean<Object>, BeanFactoryAware, InitializingBean {
190 
191 	private Class<?> serviceLocatorInterface;
192 
193 	private Constructor<Exception> serviceLocatorExceptionConstructor;
194 
195 	private Properties serviceMappings;
196 
197 	private ListableBeanFactory beanFactory;
198 
199 	private Object proxy;
200 
201 
202 	/**
203 	 * Set the service locator interface to use, which must have one or more methods with
204 	 * the signatures {@code MyType xxx()} or {@code MyType xxx(MyIdType id)}
205 	 * (typically, {@code MyService getService()} or {@code MyService getService(String id)}).
206 	 * See the {@link ServiceLocatorFactoryBean class-level Javadoc} for
207 	 * information on the semantics of such methods.
208 	 */
209 	public void setServiceLocatorInterface(Class<?> interfaceType) {
210 		this.serviceLocatorInterface = interfaceType;
211 	}
212 
213 	/**
214 	 * Set the exception class that the service locator should throw if service
215 	 * lookup failed. The specified exception class must have a constructor
216 	 * with one of the following parameter types: {@code (String, Throwable)}
217 	 * or {@code (Throwable)} or {@code (String)}.
218 	 * <p>If not specified, subclasses of Spring's BeansException will be thrown,
219 	 * for example NoSuchBeanDefinitionException. As those are unchecked, the
220 	 * caller does not need to handle them, so it might be acceptable that
221 	 * Spring exceptions get thrown as long as they are just handled generically.
222 	 * @see #determineServiceLocatorExceptionConstructor
223 	 * @see #createServiceLocatorException
224 	 */
225 	public void setServiceLocatorExceptionClass(Class<? extends Exception> serviceLocatorExceptionClass) {
226 		if (serviceLocatorExceptionClass != null && !Exception.class.isAssignableFrom(serviceLocatorExceptionClass)) {
227 			throw new IllegalArgumentException(
228 					"serviceLocatorException [" + serviceLocatorExceptionClass.getName() + "] is not a subclass of Exception");
229 		}
230 		this.serviceLocatorExceptionConstructor =
231 				determineServiceLocatorExceptionConstructor(serviceLocatorExceptionClass);
232 	}
233 
234 	/**
235 	 * Set mappings between service ids (passed into the service locator)
236 	 * and bean names (in the bean factory). Service ids that are not defined
237 	 * here will be treated as bean names as-is.
238 	 * <p>The empty string as service id key defines the mapping for {@code null} and
239 	 * empty string, and for factory methods without parameter. If not defined,
240 	 * a single matching bean will be retrieved from the bean factory.
241 	 * @param serviceMappings mappings between service ids and bean names,
242 	 * with service ids as keys as bean names as values
243 	 */
244 	public void setServiceMappings(Properties serviceMappings) {
245 		this.serviceMappings = serviceMappings;
246 	}
247 
248 	@Override
249 	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
250 		if (!(beanFactory instanceof ListableBeanFactory)) {
251 			throw new FatalBeanException(
252 					"ServiceLocatorFactoryBean needs to run in a BeanFactory that is a ListableBeanFactory");
253 		}
254 		this.beanFactory = (ListableBeanFactory) beanFactory;
255 	}
256 
257 	@Override
258 	public void afterPropertiesSet() {
259 		if (this.serviceLocatorInterface == null) {
260 			throw new IllegalArgumentException("Property 'serviceLocatorInterface' is required");
261 		}
262 
263 		// Create service locator proxy.
264 		this.proxy = Proxy.newProxyInstance(
265 				this.serviceLocatorInterface.getClassLoader(),
266 				new Class<?>[] {this.serviceLocatorInterface},
267 				new ServiceLocatorInvocationHandler());
268 	}
269 
270 
271 	/**
272 	 * Determine the constructor to use for the given service locator exception
273 	 * class. Only called in case of a custom service locator exception.
274 	 * <p>The default implementation looks for a constructor with one of the
275 	 * following parameter types: {@code (String, Throwable)}
276 	 * or {@code (Throwable)} or {@code (String)}.
277 	 * @param exceptionClass the exception class
278 	 * @return the constructor to use
279 	 * @see #setServiceLocatorExceptionClass
280 	 */
281 	@SuppressWarnings("unchecked")
282 	protected Constructor<Exception> determineServiceLocatorExceptionConstructor(Class<? extends Exception> exceptionClass) {
283 		try {
284 			return (Constructor<Exception>) exceptionClass.getConstructor(new Class<?>[] {String.class, Throwable.class});
285 		}
286 		catch (NoSuchMethodException ex) {
287 			try {
288 				return (Constructor<Exception>) exceptionClass.getConstructor(new Class<?>[] {Throwable.class});
289 			}
290 			catch (NoSuchMethodException ex2) {
291 				try {
292 					return (Constructor<Exception>) exceptionClass.getConstructor(new Class<?>[] {String.class});
293 				}
294 				catch (NoSuchMethodException ex3) {
295 					throw new IllegalArgumentException(
296 							"Service locator exception [" + exceptionClass.getName() +
297 							"] neither has a (String, Throwable) constructor nor a (String) constructor");
298 				}
299 			}
300 		}
301 	}
302 
303 	/**
304 	 * Create a service locator exception for the given cause.
305 	 * Only called in case of a custom service locator exception.
306 	 * <p>The default implementation can handle all variations of
307 	 * message and exception arguments.
308 	 * @param exceptionConstructor the constructor to use
309 	 * @param cause the cause of the service lookup failure
310 	 * @return the service locator exception to throw
311 	 * @see #setServiceLocatorExceptionClass
312 	 */
313 	protected Exception createServiceLocatorException(Constructor<Exception> exceptionConstructor, BeansException cause) {
314 		Class<?>[] paramTypes = exceptionConstructor.getParameterTypes();
315 		Object[] args = new Object[paramTypes.length];
316 		for (int i = 0; i < paramTypes.length; i++) {
317 			if (paramTypes[i].equals(String.class)) {
318 				args[i] = cause.getMessage();
319 			}
320 			else if (paramTypes[i].isInstance(cause)) {
321 				args[i] = cause;
322 			}
323 		}
324 		return BeanUtils.instantiateClass(exceptionConstructor, args);
325 	}
326 
327 
328 	@Override
329 	public Object getObject() {
330 		return this.proxy;
331 	}
332 
333 	@Override
334 	public Class<?> getObjectType() {
335 		return this.serviceLocatorInterface;
336 	}
337 
338 	@Override
339 	public boolean isSingleton() {
340 		return true;
341 	}
342 
343 
344 	/**
345 	 * Invocation handler that delegates service locator calls to the bean factory.
346 	 */
347 	private class ServiceLocatorInvocationHandler implements InvocationHandler {
348 
349 		@Override
350 		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
351 			if (ReflectionUtils.isEqualsMethod(method)) {
352 				// Only consider equal when proxies are identical.
353 				return (proxy == args[0]);
354 			}
355 			else if (ReflectionUtils.isHashCodeMethod(method)) {
356 				// Use hashCode of service locator proxy.
357 				return System.identityHashCode(proxy);
358 			}
359 			else if (ReflectionUtils.isToStringMethod(method)) {
360 				return "Service locator: " + serviceLocatorInterface.getName();
361 			}
362 			else {
363 				return invokeServiceLocatorMethod(method, args);
364 			}
365 		}
366 
367 		private Object invokeServiceLocatorMethod(Method method, Object[] args) throws Exception {
368 			Class<?> serviceLocatorMethodReturnType = getServiceLocatorMethodReturnType(method);
369 			try {
370 				String beanName = tryGetBeanName(args);
371 				if (StringUtils.hasLength(beanName)) {
372 					// Service locator for a specific bean name
373 					return beanFactory.getBean(beanName, serviceLocatorMethodReturnType);
374 				}
375 				else {
376 					// Service locator for a bean type
377 					return beanFactory.getBean(serviceLocatorMethodReturnType);
378 				}
379 			}
380 			catch (BeansException ex) {
381 				if (serviceLocatorExceptionConstructor != null) {
382 					throw createServiceLocatorException(serviceLocatorExceptionConstructor, ex);
383 				}
384 				throw ex;
385 			}
386 		}
387 
388 		/**
389 		 * Check whether a service id was passed in.
390 		 */
391 		private String tryGetBeanName(Object[] args) {
392 			String beanName = "";
393 			if (args != null && args.length == 1 && args[0] != null) {
394 				beanName = args[0].toString();
395 			}
396 			// Look for explicit serviceId-to-beanName mappings.
397 			if (serviceMappings != null) {
398 				String mappedName = serviceMappings.getProperty(beanName);
399 				if (mappedName != null) {
400 					beanName = mappedName;
401 				}
402 			}
403 			return beanName;
404 		}
405 
406 		private Class<?> getServiceLocatorMethodReturnType(Method method) throws NoSuchMethodException {
407 			Class<?>[] paramTypes = method.getParameterTypes();
408 			Method interfaceMethod = serviceLocatorInterface.getMethod(method.getName(), paramTypes);
409 			Class<?> serviceLocatorReturnType = interfaceMethod.getReturnType();
410 
411 			// Check whether the method is a valid service locator.
412 			if (paramTypes.length > 1 || void.class.equals(serviceLocatorReturnType)) {
413 				throw new UnsupportedOperationException(
414 						"May only call methods with signature '<type> xxx()' or '<type> xxx(<idtype> id)' " +
415 						"on factory interface, but tried to call: " + interfaceMethod);
416 			}
417 			return serviceLocatorReturnType;
418 		}
419 	}
420 
421 }